﻿using System;
using System.Collections.Generic;
using System.Linq;
using BuzzX8.Xmpp.StreamElements;
using BuzzX8.Xmpp.StreamElements.Factories;

namespace BuzzX8.Xmpp.Connectivity
{
    public class StreamInitializer
    {
        private IDictionary<StreamFeature, IFeatureNegotiator> featureNegotiators;
        private IFactoryProvider factoryProvider;
        private ConnectionSettings settings;
        private StreamContext streamContext;

        public StreamInitializer(ConnectionSettings settings)
        {
            if (settings == null) throw new ArgumentNullException("settings");

            featureNegotiators = new Dictionary<StreamFeature, IFeatureNegotiator>();
            this.settings = settings;
        }

        public IFactoryProvider FactoryProvider
        {
            get { return factoryProvider ?? (factoryProvider = new DefaultFactoryProvider()); }
            set { factoryProvider = value; }
        }

        public void AddNeogatiator(string featureName, string featureNamespace, IFeatureNegotiator negotiator)
        {
            featureNegotiators.Add(new StreamFeature(featureName, featureNamespace), negotiator);
        }

        public StreamContext Initialize(IXmppChannel channel)
        {
            if (string.IsNullOrEmpty(settings.Domain)) throw new ArgumentNullException();
            
            SendStreamHeader(channel);
            StreamHeader response = ReadResponse<StreamHeader>(channel);
            streamContext = new StreamContext();
            UpdateStreamContext(response);
            NegoatiateFeatures(channel);

            return streamContext;
        }

        private void SendStreamHeader(IXmppChannel channel)
        {
            StreamHeaderFactory factory = FactoryProvider.GetFactory<StreamHeaderFactory>();
            UpdateStreamHeaderModel(factory.DefaultModel);
            StreamHeader header = factory.Create();
            header.WriteXmlDeclaration = true;
            channel.Send(header);
        }

        private void UpdateStreamHeaderModel(StreamHeaderModel model)
        {
            model.From = new JID(settings.UserName, settings.Domain, "");
            model.To = settings.Domain;
            model.XmlLang = settings.DefaultCulture.TwoLetterISOLanguageName;
        }

        private void UpdateStreamContext(StreamHeader response)
        {
            streamContext.StreamId = response.Id;
        }

        private void NegoatiateFeatures(IXmppChannel channel)
        {
            StreamFeaturesList featuresList = ReadResponse<StreamFeaturesList>(channel);

            while (featuresList.HasFeatures)
            {
                StreamFeature feature = GetFeatureForNegotiation(featuresList.Features);
                NegoatiateFeature(channel, feature);
                featuresList = ReadResponse<StreamFeaturesList>(channel);
            }
        }

        private StreamFeature GetFeatureForNegotiation(ICollection<StreamFeature> features)
        {
            if (features.Any(f => f.Required)) return features.First(f => f.Required);
            return features.First();
        }

        private void NegoatiateFeature(IXmppChannel channel, StreamFeature feature)
        {
            if(!HasNegoatiatorForFeature(feature) && !feature.Required) return;
            IFeatureNegotiator negotiator = GetNegoatiator(feature.Name, feature.Namespace);
            negotiator.Negotiate(channel, feature, settings.FeaturesSettings);
        }

        private bool HasNegoatiatorForFeature(StreamFeature feature)
        {
            return featureNegotiators.Keys.Any(f => f.Name == feature.Name && f.Namespace.Equals(feature.Namespace));
        }

        private IFeatureNegotiator GetNegoatiator(string featureName, string featureNamespace)
        {
            foreach (StreamFeature feature in featureNegotiators.Keys)
            {
                if (feature.Name == featureName && feature.Namespace == featureNamespace)
                    return featureNegotiators[feature];
            }
            throw new InvalidOperationException();
        }

        private T ReadResponse<T>(IXmppChannel channel) where T : StreamElement
        {
            StreamElement response = channel.Receive();
            if (response is StreamError) OnStreamError((StreamError)response);
            if (!(response is T)) OnUnexpectedResponse(response);
            return (T) response;
        }

        private void OnStreamError(StreamError error)
        {
        }

        private void OnUnexpectedResponse(StreamElement response)
        {
        }
    }
}
